﻿/*

Copyright 2010 YiqYaq LLC
Author: Dale Low (gumbypp@yahoo.com)
 
Licensed under the Apache License, Version 2.0 (the "License"); 
you may not use this file except in compliance with the License. 
You may obtain a copy of the License at 
 
    http://www.apache.org/licenses/LICENSE-2.0 
    
Unless required by applicable law or agreed to in writing, software 
distributed under the License is distributed on an "AS IS" BASIS, 
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 
See the License for the specific language governing permissions and 
limitations under the License. 

The above license does not apply to the RadioWeave logo and name artwork,
examples of which are only included in this project for illustration
purposes.

*/

import Utils;

/**
 * \brief Module which communicates with the RadioWeave server.
 * \ingroup chumby
 */
class Server
{
    private var kMaxIterationCount:Number = 65535;
	private var kLoadTimeout:Number = 30000;

	private var STATE_IDLE:Number = 0;
	private var STATE_LOGIN:Number = 1;
	private var STATE_INITPLAY:Number = 2;
	private var STATE_PLAYLIST:Number = 3;
	private var STATE_CHANNELINFO:Number = 4;
	private var STATE_INFO:Number = 5;
	private var STATE_OPINION:Number = 6;

	private var fState:Number;                  /**< current state */  
	private var fSessionID:String;              /**< current session ID from the server */
	private var fSkipSimmerMSec:Number = 0;     /**< how long a skipped track needs to 'simmer' for before being reported to the server */  
	private var fTreatAsSkipMSec:Number = 0;    /**< how much time must elapse before a skipped traq is reported as such */
	private var fAckID:String;                  /**< last ack ID from the server */
	private var fAliasStr:String = "&alias=nahh";
	private var fSimulateLoginError:Boolean;
	private var fSimulatePlaylistError:Boolean;

	private var fNocacheIncrementer:Number;
	private var fLoader:LoadVars;
	private var fLoadTimerIteration:Number = 0;
    private var fLoadTimerID:Number = 0;
	private var fClient:Object;
	private var fTraqsAvailableCallback:Function;
	private var fServerErrorCallback:Function;
	private var fQueue:Array;
	
	private var fMixIds:Object;                         /**< array of mix IDs corresponding to the current playlist */
	private var fPlaylistTraqs:String;                  /**< comma-delimited string of traq IDs */
	private var fChannelInfo:Object;                    /**< dictionary containing per-channel meta-data */
    private var fUnreportedHeardList:Object = {};	    /**< if not '' then comma-delimited list of traqs heard to report to server */
    private var fUnreportedSkippedList:Object = {};	    /**< elements indexed by traqID, containing these elements:<br>
                                                	        .when - the milliseconds timer when they were skipped<br>
                                                	        .groupID - if this is a member of a group, save the id here */
    private var fSkippedGroups:Object = {};             /**< elements indexed by traqID, which contain a single value:
                                                            the first index of this skipped group */

    /**
        \brief Private logging function for this module.
        \param msg message to log
      */
    private function log(msg)
    {
        Utils.log("[S] " + msg);
    }

    /**
        \brief Private logging function for this module.
        \param msg warning message to log
      */
    private function logWarn(msg)
    {
        Utils.log("**S " + msg);
    }

    /**
        \brief Get the server's URL
        \return server URL
      */
	private function rootServerURL():String
	{
		//return "http://127.0.0.1:8000";		
		//return "http://192.168.1.114:8000";		
		return 'http://radioweave.com';
	}
	
    /**
        \brief Get the server's secure URL
        \return secure URL
      */
	private function secureRootServerURL():String
	{
		//return "http://127.0.0.1:8000";		
		//return "http://192.168.1.114:8000";		
	    return 'https://radioweave.com';
	}
	
    /**
        \brief Don't let things stay too long in the cache
        \return query parameter to append to the URL
      */
	private function cacheFreshnessParameter():String
	{
		fNocacheIncrementer++;
		if ( fNocacheIncrementer < 0 )   // possibly was running into problems with negative numbers. maybe.
			fNocacheIncrementer = 0;
		return "&cchrnd=" + fNocacheIncrementer;
	}
	
    /**
        \brief Get the list of unreported heard tracks
        \return comma-delimited string or '' if there are none to report
      */
    private function getUnreportedHeardList():String
    {
        var ret:String = ''
        for (var traqID:String in fUnreportedHeardList)
        {
            // work around a weird bug in old versions of Flash
            if (fUnreportedHeardList.hasOwnProperty(traqID))
            {
                if (ret != '')
                    ret += ','
                ret += traqID;
            }
        }
        return ret;
    }
	
    /**
        \brief Add any piggybacked data to the supplied URL.
        \param url base URL
        \return url with heard/skip/current traq info, if applicable
      */
	private function maybeAddPiggybackedInfoToURL(url:String):String
	{
	    // (a) add current traq position
        var currentTraqPos:String = fClient.getCurrentTraq();
        if (currentTraqPos != '')
            url += '&now=' + currentTraqPos;

        // (b) add unreported heard counts
        var unreportedHeardString:String = getUnreportedHeardList();
        if (unreportedHeardString != '')
        {
            fUnreportedHeardList = { };
            url += '&heard=' + unreportedHeardString;
        }
        
        // (c) if there are any fUnreportedSkippedList items that the server does know about yet 
        // and have been waiting long enough, then send them now
        var unreportedSkipString:String = ''
        var now:Number = (new Date()).valueOf();    
        for ( var traqID:String in fUnreportedSkippedList )
        {
            // work around a weird bug in old versions of Flash
            if ( fUnreportedSkippedList.hasOwnProperty(traqID) )
            {
                if ( fSkipSimmerMSec < ( now - fUnreportedSkippedList[traqID].when ) )
                {
                    if ( unreportedSkipString != '' )
                        unreportedSkipString += ',';
                    unreportedSkipString += traqID;
                    delete fUnreportedSkippedList[traqID];
                }
            }
        }
        
        if (unreportedSkipString != '')
            url += '&skipped=' + unreportedSkipString;
                    
        //log("-> " + url);
        return url;
	}
	
    /**
        \param version Version string
        \param name username
        \param password user's password
        \param anonBeLike true if a new anonymous user should be created who is "like" user
        \return fully qualified login URL
      */
	private function loginURLRequest(version:String, name:String, password:String, anonBeLike:Boolean):String
	{
		fState = STATE_LOGIN;
		
		var url:String = secureRootServerURL() + "/v001/api"
					   + "/?cmd=login&client=Chumby&version="
					   + version;

		if (anonBeLike)
		    url += "&name=anonymous&belike=" + name;
		else
		    url += "&name=" + name + "&pwd=" + password;

		return url;
	}
	
    /**
        \return fully qualified initplay URL
      */
	private function initplayURLRequest():String
	{
		fState = STATE_INITPLAY;
		
		var startHints:String = "&flash=" + escape(getVersion());
	
		var url:String = rootServerURL() + "/v001/api"
					   + ";jsessionid=" + fSessionID
					   + "/?cmd=initplay2"
					   + "&nsfw=x"
					   + startHints
					   + cacheFreshnessParameter()
					   + fAliasStr;
		return maybeAddPiggybackedInfoToURL(url);		
	}
	
    /**
        \param values additional query parameters.  If not required, pass ''.
        \return fully qualified playlist URL
      */
	private function playlistURLRequest(values:String):String
	{
		fState = STATE_PLAYLIST;
		
		var url:String = rootServerURL() + "/v001/api"
					   + ";jsessionid=" + fSessionID
					   + "/?cmd=playlist2"
					   + values
					   + "&ackID=" + fAckID
					   + cacheFreshnessParameter()
					   + fAliasStr;
		return maybeAddPiggybackedInfoToURL(url);
	}
	
    /**
        \param channelID comma-delimited ID(s) of the channel(s)
        \return fully qualified channelinfo URL
      */
	private function channelinfoURLRequest(channelID:String):String
	{
		fState = STATE_CHANNELINFO;
		
		var url:String = rootServerURL() + "/v001/api"
					   + ";jsessionid=" + fSessionID
					   + "/?cmd=channelinfo2"
					   + "&value=" + channelID
					   + cacheFreshnessParameter()
					   + fAliasStr;
		return maybeAddPiggybackedInfoToURL(url);
	}
	
	/**
        \param traqID comma-delimited ID(s) of the track(s)
        \return fully qualified mp3info URL
      */
	private function mp3infoURLRequest(traqID:String):String
	{
		fState = STATE_INFO;
		
		var url:String = rootServerURL() + "/v001/api"
					   + ";jsessionid=" + fSessionID
					   + "/?cmd=mp3info2"
					   + "&value=" + traqID
					   + cacheFreshnessParameter()
					   + fAliasStr;
		return maybeAddPiggybackedInfoToURL(url);
	}

    /**
        \param traqID ID of the track
        \param opinion opinion value
        \return fully qualified opinion URL
      */
    private function opinionURLRequest(traqID:String,opinion:String):String
    {
		fState = STATE_OPINION;

        var url:String = rootServerURL() + "/v001/api"
                       + ";jsessionid=" + fSessionID
                       + "/?cmd=opinion2"
                       + "&value=" + traqID
                       + "&opinion=" + opinion
                       + cacheFreshnessParameter()
                       + fAliasStr;
        return maybeAddPiggybackedInfoToURL(url);
    }

    /**
        \brief Process a single traq object from the server.
        \param info traq object
        \return true if the traq should be kept, false otherwise.
     */     
    private function processTraqGroupInfo(info:Object):Boolean
    {
        if (!info.groupID)
            return true;
            
        // add new "groupIndex" and "groupCount" properties to the traq object
        var groupID:String = info.groupID;
        var slashIdx:Number = info.groupPos.indexOf('/');
        info.groupIndex = parseInt(info.groupPos.substring(0,slashIdx));
        info.groupCount = parseInt(info.groupPos.substring(slashIdx+1));

        // if the server is sending back something we previously skipped, then remove the skipping from our memory and
        // give the user another chance to hear it (eventually, with enough skips, then server will stop resending
        // things that the user keeps skipping)
        if (Utils.findInObject(groupID, fSkippedGroups))
        {
            var firstSkippedIndex:Number = fSkippedGroups[groupID];
            if ( info.groupIndex <= firstSkippedIndex )
            {
                delete fSkippedGroups[groupID];
                for ( var traqID:String in fUnreportedSkippedList )
                {
                    if ( fUnreportedSkippedList.hasOwnProperty(traqID) )
                    {
                        if ( fUnreportedSkippedList[traqID].groupID == groupID )
                            delete fUnreportedSkippedList[traqID];
                    }   
                }
            }
            else
            {
                // this is a future segment of one the user has already skipped, 
                // so do not actually add it into our list
                logWarn("processTraqGroupInfo: ignoring " + info.traqID + " since it's part of skipped group " + info.groupID);
                return false;
            }
        }
        
        return true;
    }
    
    /**
        \brief A segment of a group has been skipped, so from our current playlist remove all entries
        \param playlist current playlist - might be modified by this function
        \param playlistIndex current position within the playlist
        \param groupID ID of group that is to be skipped
        \param firstSkippedIndex index within the group
     */     
    private function removeAllFutureSkippedSegments(playlist:Array, playlistIndex:Number, 
        groupID:String, firstSkippedIndex:Number):Void
    {
        // remove any items in playlist future for which we know from info that it's in this skipped list bad range
        for ( var idx:Number = playlistIndex + 1; idx < playlist.length; idx++ )
        {
            if ( (playlist[idx].groupID == groupID)  &&  (firstSkippedIndex < playlist[idx].groupIndex) )
            {
                logWarn("removeAllFutureSkippedSegments: removing item at position " + idx);
                playlist.splice(idx, 1);
    
                idx--;
            }
        }
    }    
    
    /**
        \brief Send the next queued request
        \return true if a request was sent, false otherwise
     */     
    private function sendNextRequest():Boolean
    {
        if (!fQueue.length || (fState != STATE_IDLE))
            return false;
            
        var req = fQueue.shift();
        switch (req.action)
        {
        case "playlist":
            log("sendNextRequest: sending playlist");
            sendRequest(playlistURLRequest(req.arg));
            return true;
            
        case "opinion":
            log("sendNextRequest: sending opinion of " + req.arg2 + " for traq " + req.arg);
            sendRequest(opinionURLRequest(req.arg, req.arg2));
            return true;
            
        default:
            break;
        }
        
        return false;
    }
    
    /**
        \brief Process a single response depending on the current state.
        \param o JSON object from the server
     */     
    private function handleResponse(o:Object):Void
    {
        var okState:Number = fState;

        fState = STATE_IDLE;
        switch(okState)
        {
            case STATE_LOGIN:
                if (o.errorType || fSimulateLoginError)
                {
                    if (fSimulateLoginError)
                    {
                        o.error = "simulated";
                        fSimulateLoginError = false;
                    }

                    logWarn("handleResponse: STATE_LOGIN/error=" + o.error);
                    fServerErrorCallback(fClient, { state: "connect", type: "app", msg: o.error });
                    return;
                }
            
                fSessionID = o["jsessionid"];			
                fSkipSimmerMSec = parseInt(o["skipSimmerSec"])*1000;
                fTreatAsSkipMSec = parseInt(o["treatAsSkipSec"])*1000;
                
                log("handleResponse: STATE_LOGIN/jsessionid=" + fSessionID + ", simmer=" + fSkipSimmerMSec);		
                sendRequest(initplayURLRequest());
                break;
                
            case STATE_INITPLAY:
                log("handleResponse: STATE_INITPLAY/ackID=" + o["ackID"]);
                fAckID = o["ackID"];

                fSkippedGroups = {};
                fUnreportedSkippedList = {};
                fChannelInfo = {};

                sendRequest(playlistURLRequest("&minCount=3"));
                break;

            case STATE_PLAYLIST:
                if (o.errorcode || fSimulatePlaylistError)
                {
                    if (fSimulatePlaylistError)
                    {
                        o.error = "simulated";
                        fSimulatePlaylistError = false;
                    }
                    
                    logWarn("handleResponse: STATE_PLAYLIST/error=" + o.error);
                    fServerErrorCallback(fClient, { state: "playlist", type: "app", msg: o.error });
                    return;
                }
                
                // build list of traqs
                fPlaylistTraqs = "";
                for (var i:Number=0; i<o["traqs"].length; i++)
                    fPlaylistTraqs += ((i>0) ? "," : "") + o["traqs"][i];
                
                log("handleResponse: STATE_PLAYLIST/traqs=" + fPlaylistTraqs);				
                
                // build list of channels that we don't know about (some traqs may be 
                // assigned to the same channel(s), so we need to uniquefy them)
                var uniqueChannels:Object = {};
                for (var i:Number=0; i<o["mixids"].length; i++)
                    if (!Utils.findInObject(o["mixids"][i], fChannelInfo) && (o["mixids"][i] != "x"))
                    {
                        // "x" = no mix associated with this traq
                        uniqueChannels[o["mixids"][i]] = true;
                    }
                
                var channels:String = "";
                for (var mix:String in uniqueChannels)
                    if (uniqueChannels.hasOwnProperty(mix))
                        channels += ((channels !== "") ? "," : "") + mix;
                
                log("handleResponse: STATE_PLAYLIST/channels=" + channels);				

                fAckID = o["ackID"];
                if (fPlaylistTraqs === "")
                {                    
                    // no traqs
                    fMixIds = null;
                    fTraqsAvailableCallback(fClient, []);
                    sendNextRequest();
                }
                else
                {			
                    fMixIds = o["mixids"];
                    if (channels === "")
                        sendRequest(mp3infoURLRequest(fPlaylistTraqs));
                    else
                        sendRequest(channelinfoURLRequest(channels));
                }
                break;

            case STATE_CHANNELINFO:
                for (var i:Number=0; i<o.length; i++)
                {
                    //log(i + ": channel=" + o[i]["uid"]);
                    fChannelInfo[o[i]["uid"]] = { 
                        name: o[i]["name"], 
                        img: o[i]["img"], 
                        description: o[i]["description"] 
                        };
                }	

                sendRequest(mp3infoURLRequest(fPlaylistTraqs));
                break;
                
            case STATE_INFO:
                var keepTraqs:Array = [];
                for (var i:Number=0; i<o.length; i++)
                {
                    //log(i + ": img=" + o[i]["img"]);
                    if (processTraqGroupInfo(o[i]))
                    {
                        // add in channel meta-data
                        if (fMixIds[i] == 'x')
                        {
                            (o[i])["channelName"] = null;
                            (o[i])["channelDesc"] = null;
                            (o[i])["channelImg"] = null;
                        }
                        else
                        {
                            (o[i])["channelName"] = fChannelInfo[fMixIds[i]].name;
                            (o[i])["channelDesc"] = fChannelInfo[fMixIds[i]].description;
                            (o[i])["channelImg"] = fChannelInfo[fMixIds[i]].img;
                        }                        
                        
                        keepTraqs.push(o[i]);
                    }
                }	
                
                fTraqsAvailableCallback(fClient, keepTraqs);
                sendNextRequest();
                break;
                
            case STATE_OPINION:
                sendNextRequest();
                break;
                
            default:
                break;
        }
    }
    
    /**
        \brief Handle a server error depending on the current state.
     */     
    private function handleError():Void
    {
        var continueNextRequest:Boolean = true;
        var errorState:Number = fState;
        
        fState = STATE_IDLE;
        switch(errorState)
        {
            case STATE_LOGIN:
            case STATE_INITPLAY:
                continueNextRequest = fServerErrorCallback(fClient, { state: "connect", type: "network" });
                break;            

            case STATE_PLAYLIST:                
            case STATE_INFO:					
                continueNextRequest = fServerErrorCallback(fClient, { state: "playlist", type: "network" });
                break;            
                
            case STATE_OPINION:                
            default:
                break;
        }        
            
        if (continueNextRequest)
            sendNextRequest();
    }
    
    /**
        \brief Don't report this traq as skipped (if it was previously),
               and stop ignoring traqs from this traq's group (if applicable)
        \param info traq object
     */
	private function undoSkip(info:Object):Void
	{
        delete fUnreportedSkippedList[info.traqID];

        if ( (info.groupID != null)  &&  Utils.findInObject(info.groupID, fSkippedGroups) )
        {
            delete fSkippedGroups[info.groupID];
            for ( var traqID:String in fUnreportedSkippedList )
            {
                if ( fUnreportedSkippedList.hasOwnProperty(traqID) )
                {
                    if ( fUnreportedSkippedList[traqID].groupID == info.groupID )
                        delete fUnreportedSkippedList[traqID];
                }                    
            }
        }
	}
	
    /**
        \brief Check the status of a network request.
      */
    function checkRequest(iteration):Void
    {
        if (!fLoadTimerID || (fLoadTimerIteration != iteration))
        {
            logWarn("checkRequest: cancelled timer or discarding old event (got " + iteration + " vs. expected " + fLoadTimerIteration);
            return;
        }

        clearInterval(fLoadTimerID);
        fLoadTimerID = 0;
        
        logWarn("checkRequest: timeout in state " + fState);

        // delete old loader callbacks
        var rc = delete fLoader.onData;
        if (!rc)
            logWarn("checkRequest: failed to delete fLoader.onData");

        // create a new loader for the next request
        initLoader();
        handleError();
    }
    
    /**
        \brief Load the specified URL and check for timeouts
        \param url url to load
      */
    private function sendRequest(url)
    {
        if (fLoadTimerID)
            clearInterval(fLoadTimerID);

        if (++fLoadTimerIteration >= kMaxIterationCount)
            fLoadTimerIteration = 0;
        fLoadTimerID = setInterval(this, "checkRequest", kLoadTimeout, fLoadTimerIteration);
        fLoader.load(url);
    }
	
    /**
        \brief Create a new network request object and attach callbacks
      */
    private function initLoader()
    {
		var self:Object = this;

		fLoader = new LoadVars();
		fLoader.onData = function(thedata) 
		{
            if (self.fLoadTimerID)
            {
                clearInterval(self.fLoadTimerID);
                self.fLoadTimerID = 0;
            }
            else
            {
				self.logWarn("initLoader.onData: ignoring due to previous timeout");
				return;
            }

			if (thedata == undefined)
			{
				self.logWarn("initLoader.onData: failed to load, state=" + self.fState);
				self.handleError();
				return;
			}
			
			try 
			{
				var o:Object = JSON.parse(thedata);
				//self.log(Utils.dumpObject(o));
				
				self.handleResponse(o);
			} 
			catch (ex) 
			{
				self.logWarn("initLoader.onData: EXCEPTION " + ex.name+":"+ex.message+":"+ex.at+":"+ex.text);
				self.handleError();
			}
		};		
    }

    // public methods	
    
    /**
        \brief Constructor
        \param client context object that is passed to callbacks
        \param traqsAvailable callback with the following signature: function(context:Object, data:Object):Void
        \param serverError callback with the following signature: function(context:Object, errorInfo:Object):Boolean
     */
	public function Server(client:Object, traqsAvailable:Function, serverError:Function)
	{
	    fSimulateLoginError = false;        // enable to test login error recovery
		fSimulatePlaylistError = false;     // enable to test playlist error recovery

        fClient = client;
		fTraqsAvailableCallback = traqsAvailable;
		fServerErrorCallback = serverError;
		fQueue = new Array();
		fNocacheIncrementer = Math.floor(Math.random() * 0x7FFFFFFFF);
	}
    	
	/**
	    \brief Login to the server.  The traqsAvailable or serverError callback will be
	           notified with the disposition.
        \param version Version string
        \param user username
        \param pwd user's password
        \param anonBeLike true if a new anonymous user should be created who is "like" user
	 */
	public function login(version:String, user:String, pwd:String, anonBeLike:Boolean):Void
	{
        log("version: " + getVersion());
		log("login: " + user);

        initLoader();
		sendRequest(loginURLRequest(version, user, pwd, anonBeLike));
	}
	
	/**
	    \brief Request more traqs from the server (the request may be queued).
	           The traqsAvailable or serverError callback will be notified with the disposition.
        \return true if request sent immediately, false if queued. 
	 */
	public function fetchMoreTraqs():Boolean
	{
	    if (fState != STATE_IDLE)
	    {
	        logWarn("fetchMoreTraqs: state is not idle, state=" + fState);

    	    if (fState == STATE_PLAYLIST)
    	    {
                logWarn("fetchMoreTraqs: ignoring duplicate playlist request (active playlist)");
                return false;
    	    }
	        
            for (var i:String in fQueue)
                if ( fQueue.hasOwnProperty(i) )
                {
                    if (fQueue[parseInt(i)].action == "playlist")
                    {
                        logWarn("fetchMoreTraqs: ignoring duplicate playlist request (previously queued)");
                        return false;
                    }
                }

	        fQueue.push({ action:"playlist", arg:"&minCount=3" });
	        return false;
	    }
	    
        sendRequest(playlistURLRequest("&minCount=3"));
	    return true;
	}
	
    /**
        \param imageFile filename provided by a previous server response
        \return fully qualified image request URL
      */
	public function imageURLRequest(imageFile:String):String
	{
		var url:String = rootServerURL() + "/v001/api"
					   + ";jsessionid=" + fSessionID
					   + "?cmd=img2&size=100"
					   + "&value=" + imageFile
					   + cacheFreshnessParameter()
					   + fAliasStr;
		return maybeAddPiggybackedInfoToURL(url);
	}
	
    /**
        \param traqID ID of the traq
        \return fully qualified mp3info URL
      */
	public function mp3URLRequest(traqID:String):String
	{
	    // always want high-quality traqs
		var speed:Number = 999;
	
		var url:String = rootServerURL() + "/v001/api"
					   + ";jsessionid=" + fSessionID
					   + "/?cmd=mp3file2"
					   + "&value=" + traqID
					   + '&speed=' + speed
					   + cacheFreshnessParameter()
					   + fAliasStr;
		return maybeAddPiggybackedInfoToURL(url);
	}
	
    /**
        \return how much time must elapse before a skipped traq is reported as such
      */
	public function getTreatAsSkipMSec():Number
	{
	    return fTreatAsSkipMSec;
	}
	
    /**
        \brief Report a user's opinion.
        \param info traq object
        \param opinion user's opinion
        \return true if request sent immediately, false if queued. 
      */
	public function reportOpinion(info:Object, opinion:String):Boolean
	{
	    if (fState != STATE_IDLE)
	    {
	        logWarn("reportOpinion: state is not idle, state=" + fState);
	        
	        fQueue.push({ action:"opinion", arg:info.traqID, arg2:opinion });
	        return false;
	    }
	    
        sendRequest(opinionURLRequest(info.traqID, opinion));
	    return true;
	}

    /**
        \brief Report the traq as heard.  This will be piggybacked
               on the next request.
        \param info traq object
      */
	public function reportHeard(info:Object):Void
	{
        fUnreportedHeardList[info.traqID] = true;
        
        undoSkip(info);
	}

    /**
        \brief Report the track as skipped and also remove any related items
               in the supplied and future playlists.
        \param playlist current playlist - may be modified after the index
        \param playlistIndex current playlist position
    */
	public function reportSkipped(playlist:Array, playlistIndex:Number):Void
	{
	    var info:Object = playlist[playlistIndex];
	    
        fUnreportedSkippedList[info.traqID] = {
            when:(new Date()).valueOf(),
            groupID:(info.groupID==null ? 0 : info.groupID)
            };

        if ( (info.groupID != null)  &&  !Utils.findInObject(info.groupID, fSkippedGroups)  &&  
            (info.groupIndex < info.groupCount) )
        {
            log("reportSkipped: adding new entry to fSkippedGroups: " + info.groupID);

            // this is not the last in the segment, and is not in skippedGroups, 
            // so store in skippedGroups to know to skip the rest
            fSkippedGroups[info.groupID] = info.groupIndex;
            removeAllFutureSkippedSegments(playlist, playlistIndex, info.groupID, info.groupIndex);
        }
	}
}
